#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <netdb.h>
#include <linux/ip.h>
#include <linux/icmp.h>
#include <linux/types.h>
#include <arpa/inet.h>
#include <sys/time.h>
int create_socket();
struct sockaddr_in get_destination(char* hostname);
void send_echo_request(int sockfd,struct sockaddr_in dest,__be16 id, __be16 seq_nr, __u8 ttl);

int wait_for_reply(int sockfd,__be16 id, __be16 seq_nr,struct in_addr* from);

#define MAX_TTL 64

int main(int argc, char* argv[]){
  setvbuf(stdout, NULL, _IONBF, 0); // keine Fehlerbehandlung (wenns nicht geht dann halt nicht)
  
  if(argc!=2){
    printf("USAGE: %s HOSTNAME\n",argv[0]);
    exit(EXIT_FAILURE);
  }
  char* target = argv[1];
  
  printf("HOSTNAME: %s\n",target);
  
  int sockfd = create_socket();
  
  struct sockaddr_in dest = get_destination(target);
  

  __u8 ttl=0;
  __be16 seq_nr=0;
  __be16 id=getpid();
  for(ttl=0;ttl<=MAX_TTL;ttl++){
    printf("(%d):\t",ttl);
    
    int cnt;
    struct in_addr from_;
    int reply_received=0;
    int pings[3]={0,0,0};
    for(cnt=0;cnt<3;cnt++){
      struct in_addr from;
      send_echo_request(sockfd,dest,id,seq_nr,ttl);
      int ping = wait_for_reply(sockfd,id,seq_nr,&from);
      if(ping > 0){
	reply_received++;
	pings[cnt]=ping;
	from_=from;
	printf("%d\t",ping);
      }else{
	printf("*\t");
      }
      seq_nr++;
      
    }
    
    if(reply_received){
      printf("%d\t%s",(pings[0]+pings[1]+pings[2])/reply_received,inet_ntoa(from_));
      
      
      struct hostent* he;
      he = gethostbyaddr(&from_,sizeof(struct in_addr),AF_INET);
      if(he != NULL){
	printf("\t%s\n",he->h_name);
      }else{
	printf("\n");
      }
      if(from_.s_addr == dest.sin_addr.s_addr)
	break;
    }else{
      printf("*\t-\n");
    }
  }
  
  close(sockfd);
  return EXIT_SUCCESS;
}

int create_socket(){
  int sockfd;
  
  if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) <0){
    printf("ERROR: cannot create socket");
    exit(EXIT_FAILURE);
  }
  int optval = 1;
  if(setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &optval, sizeof(int)) <0){
    printf("ERROR: setting option IP_HDRINCL for socket");
    exit(EXIT_FAILURE);
  }
  
  struct timeval timeout;
  memset(&timeout,0,sizeof(timeout));
  timeout.tv_sec=3;
  
  if(setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO, &timeout,sizeof(timeout)) < 0){
    printf("ERROR: setting timeout for socket");
    exit(EXIT_FAILURE);
  }
  return sockfd;
}

struct sockaddr_in get_destination(char* hostname){
  struct hostent *destinationaddress;
  destinationaddress = gethostbyname(hostname);
  if(destinationaddress==NULL){
    printf("ERROR: host:%s not found\n",hostname);
    exit(EXIT_FAILURE);
  }
  struct sockaddr_in dest;
  dest.sin_family = AF_INET;
  bcopy(
    destinationaddress->h_addr,
    &(dest.sin_addr.s_addr),
    destinationaddress->h_length
  );
  
  return dest;
}




void send_echo_request(int sockfd,struct sockaddr_in dest,__be16 id, __be16 seq_nr, __u8 ttl){
  char packet[sizeof(struct iphdr)+sizeof(struct icmphdr)];
  struct iphdr* ip;
  struct icmphdr* icmp;
  
  ip=(struct iphdr*)packet;
  icmp=(struct icmphdr*)(ip+1);
  
  ip->ihl          = 5;
  ip->version      = IPVERSION;
  ip->tos          = 0;
  ip->id           = 0;
  ip->frag_off     = 0;
  ip->ttl          = ttl;
  ip->protocol     = IPPROTO_ICMP;
  ip->check        = 0; // check
  ip->saddr        = 0; // und saddr werden automatisch eingefügt
  ip->daddr        = dest.sin_addr.s_addr;
  
  icmp->type               = ICMP_ECHO;
  icmp->code               = 0;
  icmp->un.echo.id         = htons(id);
  icmp->un.echo.sequence   = htons(seq_nr);
  
  unsigned int chk = ((icmp->code << 8) + icmp->type) + icmp->un.echo.id + icmp->un.echo.sequence;
  chk = (chk >> 16) + (chk & 0xffff);
  chk += (chk >> 16);
  icmp->checksum = ~chk;
  
  if(sendto(sockfd, packet, sizeof(struct iphdr)+sizeof(struct icmphdr), 0, (struct sockaddr *)&dest, sizeof(struct sockaddr)) <0){
    printf("ERROR send\n");
    exit(EXIT_FAILURE);
  }
}

int correct_checksum(__be16* start,int length){
  unsigned int sum=0;
  while(length>1){
    sum += *start;
    start++;
    length-=2;
  }
  if(length>0){
    sum+=((__be16)(*(char*)start)) << 8;
  }
  
  sum = (sum >> 16) +(sum &0xffff);
  sum += (sum >> 16);
  __be16 chk = ~sum;
  
  if(chk==0)
    return 1;
  else
    return 0;
}

int wait_for_reply(int sockfd,__be16 id, __be16 seq_nr,struct in_addr* from){
  struct timeval start;
  struct timeval end;
  
  char buffer[2*(sizeof(struct iphdr)+sizeof(struct icmphdr))];
  gettimeofday(&start,NULL);
  while(1){
    
    int bytes_receved;
    if((bytes_receved=recvfrom(sockfd, buffer, 2*(sizeof(struct iphdr)+sizeof(struct icmphdr)), 0, NULL,NULL))<0){
      return -1; //timeout
      //TODO andere Fehler abfangen/getrennt behandeln
    }
    
    gettimeofday(&end,NULL);
    
    if(bytes_receved < (sizeof(struct iphdr) + sizeof(struct icmphdr)))
      continue;
    
    struct iphdr* ip;
    struct icmphdr* icmp;
    
    ip=(struct iphdr*)buffer;
    

    if(!correct_checksum((__be16*)ip,ip->ihl*4))
      continue;
    if(ip->ihl!=5)
      continue;
    if(ip->version!=IPVERSION)
      continue;
    if(ip->protocol!=IPPROTO_ICMP)
      continue;
    
    icmp=(struct icmphdr*)(buffer+4*ip->ihl);
    
    if(icmp->type!=ICMP_TIME_EXCEEDED && icmp->type!=ICMP_ECHOREPLY)
      continue;
    if(icmp->type==ICMP_ECHOREPLY){
      if(!correct_checksum((__be16*)icmp,sizeof(struct icmphdr)))
	continue;
      if(icmp->un.echo.id != htons(id))
	continue;
      if(icmp->un.echo.sequence != htons(seq_nr))
	continue;
    }
    if(icmp->type==ICMP_TIME_EXCEEDED){
      if(bytes_receved < (2*(sizeof(struct iphdr) + sizeof(struct icmphdr))))
	continue;
      
      struct iphdr* inner_ip;
      struct icmphdr* inner_icmp;
      
      inner_ip=(struct iphdr*)(buffer+sizeof(struct iphdr)+sizeof(struct icmphdr));
      inner_icmp=(struct icmphdr*)(buffer+sizeof(struct iphdr)+sizeof(struct icmphdr)+sizeof(struct iphdr));
      
      if(!correct_checksum((__be16*)icmp,sizeof(struct icmphdr)+inner_ip->ihl*4+8))
	continue;
      
      if(inner_ip->ihl!=5)
	continue;
      if(inner_ip->version!=IPVERSION)
	continue;
      if(inner_ip->protocol!=IPPROTO_ICMP)
	continue;
      
      if(inner_icmp->type != ICMP_ECHO)
	continue;
      if(inner_icmp->un.echo.id!=htons(id))
	continue;
      if(inner_icmp->un.echo.sequence!=htons(seq_nr))
	continue;
      
    }
    
    from->s_addr = ip->saddr;
    return end.tv_usec-start.tv_usec +(end.tv_sec-start.tv_sec)*1000000;
  }
}